package com.amadeus.spin.standard.util;

import java.util.ArrayList;
import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFactoryConfigurationException;

import net.sf.saxon.lib.NamespaceConstant;

import org.apache.log4j.Logger;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.traversal.NodeIterator;

public class XPathHelper {

  private static final Logger LOG = Logger.getLogger(XPathHelper.class);

  private static XPath xpath;

  private static XPath getInstance() {
    if (xpath == null) {
      XPathFactory factory;
      try {
        factory = XPathFactory.newInstance(NamespaceConstant.OBJECT_MODEL_SAXON);
        xpath = factory.newXPath();
      }
      catch (XPathFactoryConfigurationException e) {
        LOG.error("Cannot instanciate Xpath factory", e);
      }
    }
    return xpath;
  }

  /**
   * Retrieve a list of values using XPath expressions
   *
   * @param path
   *          XPath expression
   * @return NodeIterator list of nodes or null.
   */
  public static NodeIterator selectNodeIterator(Node doc, String path) {
    NodeIterator result = null;
    try {
      result = XPathAPI.selectNodeIterator(doc, path);
    }
    catch (TransformerException e) {
      LOG.error("Cannot find '" + path + "': " + e.getMessage());
    }
    return result;
  }

  /**
   * Select a Node from it's xpath
   * 
   * @param doc
   * @param expression
   * @return
   */
  public static Node selectNode(Document doc, String expression) {
    Node result = null;
    if (validateXPathExpression(expression)) {
      try {
        result = (Node)getInstance().evaluate(expression, doc, XPathConstants.NODE);
      }
      catch (XPathExpressionException e) {
        LOG.error("Could not find Node for xpath " + expression);
      }
    }
    return result;
  }

  /**
   * validates that the expression is xpath 2.0 compliant. Returns true if valid.
   * 
   * @param expression
   * @return
   */
  private static boolean validateXPathExpression(String expression) {
    try {
      getInstance().compile(expression);
    }
    catch (XPathExpressionException e) {
      LOG.error("Expression is not a valid xpath 2.0 expression : " + expression);
      return false;
    }
    return true;
  }

  public static NodeList selectNodeList(Document doc, String expression) {
    NodeList result = null;
    if (validateXPathExpression(expression)) {
      try {
        result = (NodeList)getInstance().evaluate(expression, doc, XPathConstants.NODESET);
      }
      catch (XPathExpressionException e) {
        LOG.error("Could not find NodeList for xpath " + expression);
      }
    }
    return result;
  }

  /**
   * Gets the string value using XPath 22.0
   *
   * @param doc
   * @param expression
   * @return
   */
  public static String selectNodeValue(Document doc, String expression) {
    String result = null;
    if (validateXPathExpression(expression)) {
      try {
        result = selectNodeValue(doc, expression, XPathConstants.NODE);
      }
      catch (XPathExpressionException e) {
        try {
          result = selectNodeValue(doc, expression, XPathConstants.STRING);
        }
        catch (XPathExpressionException e1) {
          LOG.error("Could not find value for xpath " + expression);
        }
      }
    }
    return result;
  }

  /**
   * Fins the node string value using the QName, in order to know which type of element is searched
   * 
   * @param doc
   * @param expression
   * @param result
   * @param type
   * @return
   * @throws XPathExpressionException
   */
  private static String selectNodeValue(Document doc, String expression, QName type)
      throws XPathExpressionException {
    String result = null;
    Object nodeResult = getInstance().evaluate(expression, doc, type);
    if (nodeResult instanceof Node) {
      result = getStringValueFromNode((Node)nodeResult);
    }
    else if (nodeResult instanceof String) {
      result = (String)nodeResult;
    }
    return result;
  }

  /**
   * Retrieves the String value of a node. Works only on text and CDATA nodes
   * 
   * @param node
   * @return
   */
  private static String getStringValueFromNode(Node node) {
    String result = null;
    if (node != null) {
      NodeList list = node.getChildNodes();
      for (int i = 0; i < list.getLength(); i++) {
        Node node2 = list.item(i);
        if (node2.getNodeType() == Node.TEXT_NODE || node2.getNodeType() == Node.CDATA_SECTION_NODE) {
          result = node2.getNodeValue();
        }
      }
    }
    return result;
  }

  /**
   * Retrieve a list of values using XPath expressions. Convert NodeIterator into a list of String.
   * 
   * @param path
   *          XPath expression
   * @return List of string values of the nodes
   */
  public static List<String> getProperties(Node document, String path) {
    ArrayList<String> result = new ArrayList<String>();
    NodeIterator i = XPathHelper.selectNodeIterator(document, path);
    if (i != null) {
      for (Node n = i.nextNode(); n != null; n = i.nextNode()) {
        result.add(n.getFirstChild().getNodeValue());
      }
    }
    return result;
  }

  /**
   * Retrieve a specific node value
   * 
   * @param path
   *          XPath expression
   * @return string value of the node or null if not found.
   */
  public static String getProperty(Node document, String path) {
    try {
      return getProperties(document, path).get(0);
    }
    catch (Exception e) {
      return null;
    }
  }

}
